﻿namespace Cellfish.Toolbox.Facts.Tasks
{
    using System;
    using System.Threading;
    using System.Threading.Tasks;

    using Cellfish.Test;
    using Cellfish.Test.Fakes.System;
    using Cellfish.Toolbox.Tasks;

    using Microsoft.VisualStudio.TestTools.UnitTesting;

    [TestClass]
    public class ToApmExtensionsFacts
    {
        [TestMethod]
        public void Given_null_task_When_converted_to_APM_Then_ArgumentNullException_is_thrown()
        {
            Task task1 = null;
            Task<int> task2 = null;
            Ensure.Throws<ArgumentNullException>(() => task1.ToApm(), "Failed for Task.");
            Ensure.Throws<ArgumentNullException>(() => task2.ToApm(), "Failed for Task<T>.");
        }

        [TestMethod]
        public void Given_null_async_result_When_ending_task_Then_ArgumentNullException_is_thrown()
        {
            IAsyncResult result = null;
            Ensure.Throws<ArgumentNullException>(() => result.EndTask(), "Fails for Task.");
            Ensure.Throws<ArgumentNullException>(() => result.EndTask<int>(), "Fails for Task<T>.");
        }

        [TestMethod]
        public void Given_async_result_that_is_not_task_When_ending_task_Then_ArgumentException_is_thrown()
        {
            var result = new FakeAsyncResult();
            Ensure.Throws<ArgumentException>(() => result.EndTask(), "Failed for Task.");
            Ensure.Throws<ArgumentException>(() => result.EndTask<int>(), "Failed for Task<T>.");
        }

        [TestMethod]
        public void Given_a_task_with_bool_result_When_ending_task_with_int_result_Then_ArgumentException_is_thrown()
        {
            var task = Task.FromResult(true);
            Ensure.Throws<ArgumentException>(() => task.EndTask<int>());
        }

        [TestMethod]
        public void Given_a_completed_task_When_converting_to_APM_with_state_Then_callback_has_given_state_and_task_can_be_ended()
        {
            var task = (Task)Task.FromResult(true);
            var state = "MyState";
            var result = CallAndWaitForCompletion(cb => task.ToApm(cb, state));
            Assert.AreEqual(result.AsyncState.ToString(), state, "State is wrong.");
            result.EndTask();
        }

        [TestMethod]
        public void Given_a_completed_task_with_result_When_converting_to_APM_with_state_Then_callback_has_given_state_and_task_can_be_ended()
        {
            var task = Task.FromResult(42);
            var state = "MyState";
            var result = CallAndWaitForCompletion(cb => task.ToApm(cb, state));
            Assert.AreEqual(result.AsyncState.ToString(), state, "State is wrong.");
            Assert.AreEqual(42, result.EndTask<int>(), "Returned result is wrong.");
        }

        [TestMethod]
        public void Given_a_completed_task_When_converting_to_APM_with_no_state_Then_callback_has_no_state_and_task_can_be_ended()
        {
            var task = (Task)Task.FromResult(true);
            var result = CallAndWaitForCompletion(cb => task.ToApm(cb));
            Assert.IsNull(result.AsyncState, "State is wrong.");
            result.EndTask();
        }

        [TestMethod]
        public void Given_a_completed_task_with_result_When_converting_to_APM_with_no_state_Then_callback_has_no_state_and_task_can_be_ended()
        {
            var task = Task.FromResult(42);
            var result = CallAndWaitForCompletion(cb => task.ToApm(cb));
            Assert.IsNull(result.AsyncState, "State is wrong.");
            Assert.AreEqual(42, result.EndTask<int>(), "Returned result is wrong.");
        }

        [TestMethod]
        public void Given_a_faulted_task_When_converting_to_APM_with_state_Then_callback_has_state_and_end_task_throws_exception()
        {
            var tcs = new TaskCompletionSource<bool>();
            tcs.SetException(new Exception("FAILURE"));
            var state = "MyState";
            var task = (Task)tcs.Task;
            var result = CallAndWaitForCompletion(cb => task.ToApm(cb, state));
            Assert.AreEqual(result.AsyncState.ToString(), state, "State is wrong.");
            Ensure.Throws<Exception>(() => result.EndTask());
        }

        [TestMethod]
        public void Given_a_faulted_task_with_result_When_converting_to_APM_with_state_Then_callback_has_state_and_end_task_throws_exception()
        {
            var tcs = new TaskCompletionSource<int>();
            tcs.SetException(new Exception("FAILURE"));
            var state = "MyState";
            var task = tcs.Task;
            var result = CallAndWaitForCompletion(cb => task.ToApm(cb, state));
            Assert.AreEqual(result.AsyncState.ToString(), state, "State is wrong.");
            Ensure.Throws<Exception>(() => result.EndTask<int>());
        }

        [TestMethod]
        public void Given_a_cancelled_task_When_converting_to_APM_with_state_Then_callback_has_state_and_end_task_throws_exception()
        {
            var tcs = new TaskCompletionSource<bool>();
            tcs.SetCanceled();
            var state = "MyState";
            var task = (Task)tcs.Task;
            var result = CallAndWaitForCompletion(cb => task.ToApm(cb, state));
            Assert.AreEqual(result.AsyncState.ToString(), state, "State is wrong.");
            Ensure.Throws<TaskCanceledException>(() => result.EndTask());
        }

        [TestMethod]
        public void Given_a_cancelled_task_with_result_When_converting_to_APM_with_state_Then_callback_has_state_and_end_task_throws_exception()
        {
            var tcs = new TaskCompletionSource<int>();
            tcs.SetCanceled();
            var state = "MyState";
            var task = tcs.Task;
            var result = CallAndWaitForCompletion(cb => task.ToApm(cb, state));
            Assert.AreEqual(result.AsyncState.ToString(), state, "State is wrong.");
            Ensure.Throws<TaskCanceledException>(() => result.EndTask<int>());
        }

        private static IAsyncResult CallAndWaitForCompletion(Func<AsyncCallback, IAsyncResult> begin)
        {
            var callbackCalled = false;
            var mre = new ManualResetEvent(false);
            var result = begin(
                _ =>
                    {
                        callbackCalled = true;
                        mre.Set();
                    });
            Assert.IsTrue(mre.WaitOne(TimeSpan.FromMinutes(1)), "Operation did not complete in a timely manner.");
            Assert.IsTrue(callbackCalled, "Callback was not called");
            return result;
        }
    }
}
